<?php
/**
 * Created by PhpStorm.
 * User: ari
 * Date: 12/22/2014
 * Time: 10:06 PM
 */
namespace BC\Wallet\Bitcoin;
use BC\User\Grant\DB\GrantDB;
use CPath\Build\IBuildable;
use CPath\Build\IBuildRequest;
use CPath\Data\Schema\PDO\PDOTableClassWriter;
use CPath\Data\Schema\TableSchema;
use CPath\Request\Exceptions\RequestException;
use CPath\Request\IRequest;
use CPath\Request\Request;
use CPath\UnitTest\ITestable;
use CPath\UnitTest\IUnitTestMockMethod;
use CPath\UnitTest\IUnitTestRequest;

/**
 * Class GrantEntry
 * @table bitcoin_address_lookup
 * @satoshi BTC=x/100000000
 */
define('DAY', 60*60*24);
define('BitcoinAddressLookup', __NAMESPACE__ . '\\BitcoinAddressLookup');
class BitcoinAddressLookup implements IBuildable, ITestable
{
	const MOCK_UPDATE = ':mock-blockchain-update';

	const HARD_CHECK_LIMIT = 10;
	const BALANCE_CHECK_LIMIT = DAY;
	const CONFIRMATION_LIMIT = 6;
	const UPDATE_LIMIT = 50;

	const URL_MULTI_ADDRESS_FORMAT = 'https://blockchain.info/multiaddr?format=json&active=%s';

	/**
	 * @column PRIMARY KEY
	 * @select
	 */
	protected $address;

	/**
	 * @column INTEGER
	 * @select
	 * @insert
	 * @update
	 */
	protected $last_check;

	/**
	 * @column INTEGER
	 * @select
	 * @insert
	 * @update
	 */
	protected $balance;

	/**
	 * @column INTEGER
	 * @select
	 * @insert
	 * @update
	 */
	protected $received_by_address;

	/**
	 * @column INTEGER
	 * @select
	 * @insert
	 * @update
	 */
	protected $sent_by_address;

	public function getBalance() {
		return intval($this->balance);
	}

	public function getAddress() {
		return $this->address;
	}

	/**
	 * @return mixed
	 */
	public function getReceivedByAddress() {
		return $this->received_by_address;
	}

	/**
	 * @return mixed
	 */
	public function getSentByAddress() {
		return $this->sent_by_address;
	}

	public function needsUpdate() {
		return intval($this->last_check) < time() - static::BALANCE_CHECK_LIMIT;
	}

	public function getUpdateURL(array $addresses) {
		return sprintf(static::URL_MULTI_ADDRESS_FORMAT, implode('|', $addresses));
	}

	public function updateLastCheck() {
		$Table = new BitcoinAddressLookupTable();
		$Table
			->update($Table::COLUMN_LAST_CHECK, time())
			->where($Table::COLUMN_ADDRESS, $this->getAddress());
	}

	public function updateBlockChainInfo(IRequest $Request, $force=false) {
		if($Request instanceof IUnitTestMockMethod)
			$force = true;

		if(!$this->needsUpdate() && !$force)
			return 0;

		$Table = new BitcoinAddressLookupTable();

		$addresses = array();
		$addresses[] = $this->getAddress();

		$maxLastCheck = $Table->select($Table::COLUMN_LAST_CHECK, null, "MAX(%s)")
			->fetchColumn(0, $Request);
		if($maxLastCheck > time() - static::HARD_CHECK_LIMIT && !$force) {
			$Request->log("Block chain hard limit reached: " . (time() - $maxLastCheck), $Request::ERROR);
			return 0;
		}

		$Where = $Table->where($Table::COLUMN_LAST_CHECK, time() - static::BALANCE_CHECK_LIMIT)
			->limit(static::UPDATE_LIMIT * 5);

		$c = 0;
		foreach($Where as $Lookup) {
			/** @var BitcoinAddressLookup $Lookup */
			if($Lookup->needsUpdate()) {
				$addresses[] = $Lookup->getAddress();
			} else {
				$Lookup->updateLastCheck();
				$c++;
				continue;
			}
			if(sizeof($addresses) >= static::UPDATE_LIMIT)
				break;
		}

		$contents = $this->getURLContents($Request, $addresses);

		$json = json_decode($contents, true);
		if(!is_array($json)) {
			throw new RequestException("Invalid JSON: " . $contents); }
		$addresses = $json['addresses'];
		if(sizeof($addresses) === 0)
			throw new RequestException("Addresses not found: " . $this->getAddress());
		foreach($addresses as $info) {
			if(!isset($info['address'], $info['total_received'], $info['total_sent'], $info['final_balance']))
				throw new RequestException("Invalid Address Info: " . json_encode($info));
			$Table
				->update($Table::COLUMN_BALANCE, $info['final_balance'])
				->update($Table::COLUMN_SENT_BY_ADDRESS, $info['total_sent'])
				->update($Table::COLUMN_RECEIVED_BY_ADDRESS, $info['total_received'])
				->update($Table::COLUMN_LAST_CHECK, time())
				->where($Table::COLUMN_ADDRESS, $info['address']);
			if($info['address'] === $this->getAddress()) {
				$this->balance = $info['final_balance'];
				$this->sent_by_address = $info['total_sent'];
				$this->received_by_address = $info['total_received'];
			}
			$c++;
		}
		return $c;
	}

	protected function getURLContents(IRequest $Request, Array $addresses) {
		if($Request instanceof IUnitTestMockMethod)
			return $Request->mock(self::MOCK_UPDATE, func_get_args());

		$url = $this->getUpdateURL($addresses);
		return file_get_contents($url);
	}

	// Static

	/**
	 * Handle this request and render any content
	 * @param IBuildRequest $Request the build request inst for this build session
	 * @return void
	 * @build --disable 0
	 * Note: Use doctag 'build' with '--disable 1' to have this IBuildable class skipped during a build
	 */
	static function handleBuildStatic(IBuildRequest $Request) {
		$Schema      = new TableSchema(__CLASS__);
		$ClassWriter = new PDOTableClassWriter(new GrantDB(), __NAMESPACE__ . '\BitcoinAddressLookupTable', __CLASS__);
		$Schema->writeSchema($ClassWriter);
	}

	/**
	 * Perform a unit test
	 * @param IUnitTestRequest $Test the unit test request inst for this test session
	 * @return void
	 * @test --disable 0
	 * Note: Use doctag 'test' with '--disable 1' to have this ITestable class skipped during a build
	 */
	static function handleStaticUnitTest(IUnitTestRequest $Test) {
		$Table = new BitcoinAddressLookupTable();
		$Lookup = $Table->insertOrUpdate('1EzwoHtiXB4iFwedPr49iywjZn2nnekhoj', array());
		$Lookup->updateBlockChainInfo(
			new Request($Test->getMethodName(), $Test->getPath(), $Test->getMimeType())
		);
	}
}
